Directory layout
This document defines where code, virtual environments, configuration, and logs live on the Ubuntu API server. Paths follow the Filesystem Hierarchy Standard (FHS) and common production practice for systemd-managed Python services.
Design principles
- Separate code from secrets — Application source is plain git checkout; credentials never live in the repo.
- Separate code from runtime — Virtual environments are not committed and live outside the app tree.
- Least privilege — A dedicated unprivileged user runs all APIs; root is only used for install and systemd/Nginx changes.
- Localhost-only apps — Each API listens on
127.0.0.1:<port>. Only Nginx is reachable from the network. - Predictable naming — One canonical
<api-name>slug used everywhere (directories, systemd unit, env file, Nginx filename).
<api-name> naming convention
Use lowercase kebab-case matching the repository or product name.
| Good | Avoid |
|---|---|
customer-api | CustomerAPI |
report-api | api1 |
document-api | customer_api (underscores) |
The same slug appears in:
/opt/apis/<api-name>//opt/venvs/<api-name>//etc/<api-name>//etc/systemd/system/<api-name>.service/etc/nginx/sites-available/<api-name>(or one file per subdomain)/var/log/<api-name>/(if file logging is used)
Full directory map
/
├── opt/
│ ├── apis/ # Application source trees
│ │ └── <api-name>/ # Git checkout; one directory per API
│ │ ├── app/ # (example) Python package — layout varies by repo
│ │ ├── requirements.txt
│ │ └── README.md
│ │
│ └── venvs/ # Virtual environments (not in git)
│ └── <api-name>/ # Created with python3 -m venv
│ ├── bin/
│ │ ├── python
│ │ ├── pip
│ │ └── uvicorn # or gunicorn
│ └── lib/
│
├── etc/
│ ├── <api-name>/ # Per-API host configuration
│ │ ├── <api-name>.env # Secrets and environment (mode 600)
│ │ └── README # Optional: documents non-secret env vars
│ │
│ ├── systemd/system/
│ │ └── <api-name>.service # systemd unit
│ │
│ └── nginx/
│ ├── sites-available/
│ │ └── <api-name> # Server block for this API's subdomain
│ └── sites-enabled/
│ └── <api-name> -> ../sites-available/<api-name>
│
└── var/
└── log/
└── <api-name>/ # Optional; systemd journal is default
└── app.log
Why these locations (industry rationale)
/opt/apis/<api-name>/ — application code
FHS defines /opt for add-on application software. Custom API deployments commonly use /opt/<vendor-or-app>/ or /opt/apps/. Using /opt/apis/ keeps all Python services under one parent and avoids mixing them with system packages in /usr.
Alternatives (not used here):
| Path | Typical use |
|---|---|
/srv/<api-name>/ | FHS "data for services"; common for web content, less common for Python app code |
/var/www/<api-name>/ | Legacy Apache convention; still seen but less ideal for non-PHP APIs |
/home/<user>/apps/ | Acceptable for single-developer boxes; weaker for shared ops ownership |
/opt/venvs/<api-name>/ — virtual environments
Keeping venvs outside the git tree:
- Prevents accidental commit of
lib/andbin/ - Allows
git clean/ redeploy without destroying the environment - Makes dependency upgrades explicit (
pip install -r requirements.txtin the known venv path)
Some teams use .venv inside the app directory; that works for development but the split layout scales better when multiple APIs share one server and one deploy playbook.
/etc/<api-name>/ — configuration and secrets
FHS /etc is for host-specific configuration. Storing <api-name>.env here (not in /opt/apis/) ensures:
- Secrets survive
git pull/ directory replace - File permissions can be locked down independently of code ownership
- systemd can load env via
EnvironmentFile=with a stable path
Do not store secrets in:
- The git repository
/opt/apis/<api-name>/.env(too easy to overwrite on deploy unless explicitly excluded)
/etc/systemd/system/ — service units
Standard location for administrator-defined units on systemd systems. Package-provided units live in /lib/systemd/system/; local overrides belong here.
/etc/nginx/sites-available/ — reverse proxy
Debian/Ubuntu Nginx convention: define in sites-available, enable via symlink in sites-enabled. SSL certificates are referenced from these blocks (exact cert paths depend on your existing cert layout).
/var/log/<api-name>/ — optional file logs
By default, systemd captures stdout/stderr in the journal (journalctl -u <api-name>). Add file logging under /var/log/<api-name>/ only if an API or compliance requirement needs log files on disk.
Service account
Create one shared service user for all APIs on this host:
| Field | Value |
|---|---|
| Username | apisvc |
| Home | /var/lib/apisvc (or /nonexistent with nologin shell) |
| Shell | /usr/sbin/nologin |
| Purpose | Runs all API processes; owns code and venv directories |
sudo useradd --system --home-dir /var/lib/apisvc --create-home \
--shell /usr/sbin/nologin apisvc
When to use per-API users instead: Strict isolation between untrusted apps, different teams with separate sudo boundaries, or compliance requiring separate UIDs. For a single-org internal server, one apisvc user is standard and simpler.
Ownership and permissions
Apply after creating directories for each API. Replace <api-name> with the actual slug.
| Path | Owner | Mode | Notes |
|---|---|---|---|
/opt/apis/ | root:apisvc | 775 | Group write — deploy users in apisvc can git clone |
/opt/apis/<api-name>/ | apisvc:apisvc | 755 | Entire git checkout; g+rwX after clone for git pull |
/opt/venvs/ | root:apisvc | 775 | Parent directory |
/opt/venvs/<api-name>/ | apisvc:apisvc | 755 | Created by apisvc or root then chown |
/etc/<api-name>/ | root:apisvc | 750 | Only root and apisvc read secrets |
/etc/<api-name>/<api-name>.env | root:apisvc | 640 | Secrets; group-readable by apisvc |
/var/log/<api-name>/ | apisvc:apisvc | 755 | If used |
Deploy user (e.g. gqc) runs git clone and git pull. That user must be in the apisvc group and /opt/apis must be mode 775. Without this, clone fails with “Permission denied” — do not use sudo git clone.
sudo usermod -aG apisvc gqc
# Log out and SSH back in
sudo mkdir -p /opt/apis
sudo chown root:apisvc /opt/apis
sudo chmod 775 /opt/apis
Verify after re-login:
groups # must include apisvc
touch /opt/apis/.write-test && rm /opt/apis/.write-test
After clone: sudo chown -R apisvc:apisvc /opt/apis/<api-name> and sudo chmod -R g+rwX /opt/apis/<api-name> so future git pull works.
Full Git/deploy-key setup: 07-code-management-and-git.md.
Users, docker group, and Docker install: 08-docker-install-and-users.md.
Port allocation
Assign one localhost port per API. Document assignments in the root README.md to avoid collisions.
| API (example) | Port | Notes |
|---|---|---|
| First API | 8001 | Initial deployment |
| Second API | 8002 | Increment by one |
| Third API | 8003 | ... |
Bind address is always 127.0.0.1, never 0.0.0.0.
Initial server setup (one time)
Run on the Ubuntu server as root or with sudo:
# Parent directories — 775 so gqc (apisvc group) can git clone
sudo mkdir -p /opt/apis /opt/venvs
sudo chown root:apisvc /opt/apis /opt/venvs
sudo chmod 775 /opt/apis /opt/venvs
# Service user (skip if already exists)
sudo useradd --system --home-dir /var/lib/apisvc --create-home \
--shell /usr/sbin/nologin apisvc 2>/dev/null || true
# Deploy login user (example: gqc)
sudo usermod -aG apisvc gqc
# Log out and SSH back in
Per-API directory creation is covered in 02-adding-a-new-api.md.
Relationship to Nginx and firewall
Internet / LAN
│
▼
Nginx :443 / :80 ← only these ports allowed in UFW (when enabled)
│
├── 127.0.0.1:8001 ← API 1 (not in UFW)
├── 127.0.0.1:8002 ← API 2
└── 127.0.0.1:8003 ← API 3
Nginx configuration is not under /opt/; it lives in /etc/nginx/ as described above. See 04-nginx-reverse-proxy.md.
Azure and external secrets
APIs that use Azure Storage should read credentials from /etc/<api-name>/<api-name>.env:
AZURE_STORAGE_CONNECTION_STRING=...
# or
AZURE_CLIENT_ID=...
AZURE_CLIENT_SECRET=...
AZURE_TENANT_ID=...
Never commit these values. Rotate by editing the env file and restarting the service: sudo systemctl restart <api-name>.
Docker (future / parallel path)
When APIs run in containers, /opt/venvs/ is not used. Secrets stay in /etc/<api-name>/. Nginx still proxies to 127.0.0.1:<port>. See 05-docker-strategy.md.
Quick reference for technicians
| I need to… | Location or command |
|---|---|
| Find application code | /opt/apis/<api-name>/ |
| Activate the venv | source /opt/venvs/<api-name>/bin/activate |
| Edit secrets | sudo nano /etc/<api-name>/<api-name>.env |
| Edit systemd unit | sudo nano /etc/systemd/system/<api-name>.service |
| View logs | journalctl -u <api-name> -f |
| Restart API | sudo systemctl restart <api-name> |
| Edit Nginx | /etc/nginx/sites-available/<api-name> |